package group2projman;

import group2projman.LibrarySystem.Type;

import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeMap;

class Catalogue {		
	
	/** Associates authors with the set of books by that author in the catalogue.
	 * 
	 * @note Assumes each author's name is a unique string.
	 * 
	 * TODO: fix that assumption?
	 */
	Map<String, Set<Book>> booksByAuthor = new TreeMap<String, Set<Book>>();	
	/** Associates categories with the set of books under that category in the catalogue. */
	Map<Catalogue.Category, Set<Book>> booksByCategory = new TreeMap<Catalogue.Category, Set<Book>>();
	/** Associates ISBNs with their corresponding books, if catalogued. */
	Map<Integer, Book> booksByIsbn = new TreeMap<Integer, Book>();
	/** Associates categories with the set of books by that title in the catalogue. */
	Map<String, Set<Book>> booksByTitle = new TreeMap<String, Set<Book>>();
	
	/**
	 * TODO: Unfuck.
	 *
	 * TODO: Or maybe just undo, replace with a List<String> field, allowing categories to be added/removed/checked at runtime,
	 * and adding a bit to encapsulation.
	 */
	enum Category {
		GEOGRAPHY, CRIMINOLOGY, HABERDASHERY, NEW_WEIRD, COMPARATIVE_HERPETOLOGY, MARINE_BIOLOGY, CRYPTOZOOLOGY		
	}
	
	/**
	 * 
	 * @throws AssertionException if a book with the same ISBN is already catalogued. */
	void addBook(Book book) {
		// Add book to each search tree
		// First by author
		Set<Book> s = booksByAuthor.get(book.author);
		
		if (s == null) {
			// No other books by that author. Create new Set of such books
			s = new HashSet<Book>();
			s.add(book);
			booksByAuthor.put(book.author, s);
		} else {
			// Assert book is not already in catalogue, then add to catalogue
			assert(!s.contains(book));
			s.add(book);
		}
		
		// By title
		s = booksByTitle.get(book.title);
		
		if (s == null) {
			s = new HashSet<Book>();
			s.add(book);
			booksByTitle.put(book.title, s);
		} else {
			assert(!s.contains(book));
			s.add(book);
		}
		
		// By category
		s = booksByCategory.get(book.category);
		
		if (s == null) {
			s = new HashSet<Book>();
			s.add(book);
			booksByCategory.put(book.category, s);
		} else {
			assert(!s.contains(book));
			s.add(book);
		}
		
		// By ISBN
		assert(!booksByIsbn.containsKey(book.isbn));
		
		booksByIsbn.put(book.isbn, book);
	}
	
	// TODO: Figure out shelf marks, lol
	/**
	 * 
	 * @throws AssertionException if no book with the given ISBN is catalogued.
	 */
	void addBookCopy(int isbn, BookCopy.Condition condition, BookCopy.LoanPermission loanPermission) {		
		Book book = booksByIsbn.get(isbn);
		
		assert(book != null);
		
		BookCopy copy = new BookCopy(book, new Date(), "Shelfmark Placeholder", condition, loanPermission);
	}
	
	/**
	 * @throws AssetionException if book is not catalogued (that is, if book is absent from one or more
	 * of the catalogue's search trees)
	 * 
	 * TODO: ensure this doesn't bork transaction histories. If it doesn't, assert no
	 * copies of the book remain catalogued.
	 * 
	 * TODO: I guess just move book to a "removed" array list, empty reserved queue, throw exception if copies of book remain.
	 * Do similar deal with Book.removeCopy, maybe migrate that here
	 */
	void removeBook(Book book) {
		assert(booksByAuthor.containsKey(book.author)
		&& booksByTitle.containsKey(book.title)
		&& booksByCategory.containsKey(book.category)
		&& booksByIsbn.containsKey(book.isbn));

		booksByAuthor.remove(book.author);
		booksByTitle.remove(book.title);
		booksByCategory.remove(book.category);
		booksByIsbn.remove(book.isbn);	
	}
	
	/**
	 * @note Does not verify that copy's Book is catalogued.
	 * 
	 * TODO: Change name? Less consistent, now that the addBook() overload's been renamed
	 * 
	 * TODO: Assert copy not already removed?
	 */
	void removeBook(BookCopy copy) {
		booksByIsbn.get(copy.bookId).removeCopy(copy);
	}
		
	void returnBook(BookCopy copy, Member member)
			throws NoSuchElementException {
				
		// Is the book overdue?
		Loan[] loans = member.transactionHistory.getCurrentLoans();
		Integer cost = null;
		
		for (Loan l : loans) {
			if (l.bookCopyId == copy.id);
			cost = lateFee(l, new Date());
		}
		if (cost == null)
			throw new NoSuchElementException("Loan not found");
		
		// Update member's transaction history
		int transactionId = LibrarySystem.getInstance().nextId(LibrarySystem.Type.TRANSACTION);
		member.transactionHistory.addTransaction(new Return(transactionId, copy.getId(), cost.intValue()));
		
		// Is anyone queued to reserve the Book? And is the copy still available for loan?
		Book book = booksByIsbn.get(copy.bookId);
		if (copy.reservable() && !book.queueEmpty()) {
			// Yes. Reserve copy for member and pop from queue
			Member firstInLine = book.popQueue(); 
			copy.isReserved = true;
			copy.loanedTo = firstInLine;
			firstInLine.transactionHistory.addTransaction(new Loan(LibrarySystem.getInstance().nextId(Type.TRANSACTION), copy.getId()));
		} else {
			// Nope. Reshelf
			copy.isReserved = false;
			copy.loanedTo = null;
		}
	}
	
	private int lateFee(Loan loan, Date returnDate) {
		if (!loan.dueDate.before(returnDate))
			return 0;
				
		// TODO: get diff in days		
		int daysOverdue = 0;
		
		Calendar currentDate = Calendar.getInstance();
		currentDate.setTime(loan.dueDate);
		while (currentDate.before(returnDate)) {
			currentDate.add(Calendar.DAY_OF_MONTH, 1);  
			++daysOverdue;  
		}   // TODO: figure out how to prevent fees from being incurred if due date is a millisecond before the return date
		
		return LibrarySystem.DAILY_OVERDUE_COST * daysOverdue;		
	}
	
	/**
	 * TODO: Maybe sort return value alphabetically?
	 * 
	 * TODO: Maybe do something if a Book's ISBN disagrees with the rest of the query?
	 * 
	 * @return An array (possibly empty) of books that match the search query, in no particular order.
	 * 	throws SQLException {
		// Shallow-save member
		StringBuffer result = new StringBuffer();
		result.append(saveAttributesThing(member.listAttributes(), "Members", connection));
		
		// Create transaction table, if nonexistent
		// TODO: do this
		
		// Save transaction history
		String transactionTableName = "Transactions" + member.getId();
		
		for (Transaction t : member.transactionHistory.transactions) {
			// TODO: do
		}
		
		//
	 * @throws AssertionException if all of the query's fields are null or invalid.
	 */
	Book[] searchBooks(Query query) {
		// Validate input; query must have content
		assert(query.author != null || query.category != null || query.title != null || query.isbn != 0);
		
		// Are we searching by ISBN?
		if (query.isbn != 0) {
			// Yes. Return 0-1 books, ignore other search arguments
			Book isbnResult = booksByIsbn.get(query.isbn);
			
			if (isbnResult == null)
				return new Book[0];
			else {
				Book[] returnVal = {isbnResult};
				
				return returnVal;
			}
		}
		
		// No. Return 0 or more books that match all non-null search arguments
		Set<Book> results = null;
		
		if (query.author != null)
			intersect(results, booksByAuthor.get(query.author));
		if (query.title != null)
			intersect(results, booksByTitle.get(query.title));
		if (query.category != null)
			intersect(results, booksByCategory.get(query.category));
		
		if (results == null)
			return new Book[0];
		else
			return (Book[])results.toArray();
	}
	
	/** Set intersection with special cases for null Set references.
	 * 
	 * Given two sets, removes all elements from the first which it does not share with the second. 
	 * 
	 * If the first Set is null, assigns it the value of the second (which might also be null).
	 * If the first Set is non-null and the second is null, treats the second as the empty set.
	 */
	private void intersect(Set<Book> first, Set<Book> second) {
		if (first == null)
			first = second;
		else
			if (second == null)
				first.clear();
			else
				first.retainAll(second);
	}
}
